/**
 * mutexing video frames with H264/H265 
 * and audio frames with AAC/G711a into a MP4 file,
 * and download it from browser
 */
self.importScripts("fs.js")

const isLittleEndian = true;


const EventRecordDownloadCompleted = 0x00000000;

/****frames come from websocket */
const FRAME_CONFIG              = 0x68643030;
const FRAME_IDR                 = 0x62643030;
const FRAME_PARTIAL             = 0x63643030;
const FRAME_AUDIO               = 0x62773030;
const FRAME_GPS                 = 0x73706730;
const FRAME_END                 = 0x481;


const VideoFrameLast            = -1;
const VideoFrameFirst           = 0;
const VideoFrameMiddle          = 1;


/***send data to decoder.js */
const REQUEST_NEW_DECODER        = 0x6001;
const REQUEST_FEED_DECODER_DATA  = 0x6003;
const REQUEST_FEED_GPS_DATA      = 0x6005;

/****Request */
const REQUEST_FETCH_START        = 0x6001;
const REQUEST_FETCH_RELEASE      = 0x6003;
const REQUEST_FETCH_FRAMES       = 0x6004;
/** Response */
const RESPONSE_FETCH_CLOSE       = 0x2002;
const RESPONSE_FETCH_ERROR       = 0x2003;
const RESPONSE_FETCH_PROGRESS    = 0x2004;
const RESPONSE_FETCH_WSERROR     = 0x2005;
const RESPONSE_FETCH_DONE        = 0x2006;
const RESPONSE_FETCH_READY       = 0x2007;


/**cache 6 frames and write in file */
const batchWriteLength = 6

/**
 * only supports 16 websocket connections
 */

var clearData = function(){
    if(this.localFile == null)return;
    this.localFile.release();
    this.localFile = null;
    this.isInited = false;
}

var _release = function(){
    this.clearData();
    this.websocket && this.websocket.close();
    self.close();
    console.info("fetch.js release...")
}


var _parseWebSocketMessage = function(dataView,data,frameType,length,n){
    var postData = null;
    switch(frameType){
        case FRAME_CONFIG:{//config frame
           var width = dataView.getInt32((++n) * 4,isLittleEndian);
           var height =  dataView.getInt32((++n) * 4,isLittleEndian);
           var audioType = dataView.getInt32((++n) * 4,isLittleEndian);//0:G711,1:g726,2:AAC,3:g711,
           var frameRate = dataView.getInt32((++n) * 4,isLittleEndian);
           var sampleRate = dataView.getInt32((++n) * 4,isLittleEndian);

           var bitWidthPerSample = dataView.getUint8((++n) * 4,isLittleEndian);
           
           var channelCount = dataView.getUint8(n * 4 + 1,isLittleEndian);
           var videoType = dataView.getUint8(n * 4 + 2,isLittleEndian);	//0:H264,1:H265,4:H265
           var reserved =  dataView.getUint8(n * 4 + 3,isLittleEndian);	//保留
           console.info("video=%s,frameRate=%d,width=%d,height=%d,audio=%s,sampleRate=%d,bitWidthPerSample=%d,channelCount=%d",
           videoType ,frameRate,width,height,audioType,sampleRate,bitWidthPerSample,channelCount);
           audioType = audioType == 0 ? 0 : 1;
           videoType = videoType == 0 ? 0 : 1;
           console.info("audio=",audioType == 0 ? 'G711':'AAC',"video=",videoType == 0 ? "H264":"H265");
           this.isFirstVideoFrame = true;
           sampleRate = 8000;
           channelCount = 1;
           self.postMessage({
                type:REQUEST_NEW_DECODER,
                videoType:videoType,
                audioType:audioType,
                sampleRate:sampleRate,
                channelCount: channelCount
           });
           break;
        }
        case FRAME_IDR:{//IDR frame
           var pts = dataView.getBigUint64((++n) * 4,isLittleEndian);
           pts = this._fixPTS(Number(pts));
           var result = data.slice(28);
           var index = VideoFrameMiddle;
           if(this.isFirstVideoFrame){
            this.isFirstVideoFrame = false;
           }
          if(this.isLive){
            postData = {
                type:REQUEST_FEED_DECODER_DATA,
                isAudio:false,
                data:result,
                pts:pts,
                index:index
             };
            self.postMessage(postData,[postData.data]);
          }else{
            this._writeLocalFile({
               isAudio:false,
               data:result,
               pts:pts,
               index:index
            });
            self.postMessage({
                type:RESPONSE_FETCH_PROGRESS,
                pts:pts
            });
           }
           //console.info("AAA Get IDR frame,totalSize = %d,size = %d,pts=%d",length,result.byteLength,pts)
           break;
        }
        case FRAME_PARTIAL:{//partial frame
           if(this.isFirstVideoFrame){
                return ;   
           }
           var pts = dataView. getBigUint64((++n) * 4,isLittleEndian);
           pts = Number(pts);
           pts = this._fixPTS(pts);
         
           var result = data.slice(28)
           var index = VideoFrameMiddle;
           if(this.isLive){
             postData = {
                type:REQUEST_FEED_DECODER_DATA,
                isAudio:false,
                data:result,
                pts:pts,
                index:index
             }
            self.postMessage(postData,[postData.data]);
          }
          else{
            this._writeLocalFile({
               isAudio:false,
               data:result,
               pts:pts,
               index:index
            });
            self.postMessage({
                type:RESPONSE_FETCH_PROGRESS,
                pts:pts
            });
          }
          this.cacheTotolTime = pts;
          //console.info("AAA Get partial frame,totalSize = %d,size = %d,pts=%d",length,result.byteLength,pts)
          break;
        }
        case FRAME_AUDIO:{//audio frame
           var pts = dataView. getBigUint64((++n) * 4,isLittleEndian);
           pts = this._fixPTS(Number(pts));
           var result = data.slice(28 + 4);//4byte haisi audio header
           var index = VideoFrameMiddle;
           
           if(this.isLive){
            var postData = {
                type:REQUEST_FEED_DECODER_DATA,
                isAudio:true,
                data:result,
                pts:pts,
                index:index
             };
             self.postMessage(postData,[postData.data]);
           }else{
            self.postMessage({
                type:RESPONSE_FETCH_PROGRESS,
                pts:pts
            });
             this._writeLocalFile({
                isAudio:true,
                data:result,
                pts:pts,
                index:index
             });
           }

           //console.info("AAA Get audio frame,totalSize = %d,size = %d,pts=%d",length,result.byteLength,pts)
           break;
        }
        case FRAME_GPS://GPS location
            var pts = dataView. getBigUint64((++n) * 4,isLittleEndian);
            pts = this._fixPTS(Number(pts));
            var gpsStatus = dataView.getInt8(64);
            if(gpsStatus == 65){
                n = 68;
                var latitudeDegree = dataView.getUint8(n);
                var latitudeCent = dataView.getUint8(++n);
                var longitudeDegree = dataView.getUint8(++n);
                var longitudeCent = dataView.getUint8(++n);
                var latitudeSec = dataView.getInt32(++n,true);
                n += 4;
                var longitudeSec = dataView.getInt32(n,true);
                var tmplongitude =  parseFloat(longitudeCent  / 60.00) * 10000000   + parseFloat(longitudeSec / 3600.00);
                var tmplatitude = parseFloat(latitudeCent  / 60.00) * 10000000  + parseFloat(latitudeSec / 3600.00);
                var longitude = parseFloat(longitudeDegree+"."+parseInt(tmplongitude));
                var latitude = parseFloat(latitudeDegree+"."+parseInt(tmplatitude));
                n = 88;
                const years = dataView.getUint16(n,isLittleEndian);
                n += 2;
                var  month = dataView.getUint8(n);
                var  days = dataView.getUint8(++n);
                var  hours = dataView.getUint8(++n);
                var  minutes = dataView.getUint8(++n);
                var  seconds = dataView.getUint8(++n);
                var timestamp = years + "-" + ((month >9) ? month: "0"+month) + '-' +((days >9) ? days: "0"+days) +
                    " " + ((hours >9) ? hours: "0"+hours) + ":" + ((minutes >9) ? minutes: "0"+minutes) + ":" + ((seconds >9) ? seconds: "0"+seconds)
                console.info("GPS",pts,timestamp,latitude,longitude);
                var postData = {
                    type:REQUEST_FEED_GPS_DATA,
                    pts:pts,             //playtime
                    timestamp:timestamp, //createTime
                    latitude:latitude,   
                    longitude:longitude
                };        
                self.postMessage(postData);
            }
            break;
       
        default:{
          console.error("not implemented frameType found")
          break;
        }
    }
}

var onWebSocketError = function(event){
    self.postMessage({
        type:RESPONSE_FETCH_WSERROR,
    });
    this.websocket && this.websocket.close()
    this.websocket = null;
}
var onWebSocketClosed = function(){
    self.postMessage({
        type:RESPONSE_FETCH_CLOSE,
    });
    this.websocket && this.websocket.close()
    this.websocket = null;
}
var onWebSocketOpened = function(event){
    console.info("fetch onwebSocketopened");
    this.websocket.send(this.playParams); 
}

var onWebSocketMessage = function(event){
    var data = event.data;
    //console.info("_onWebSocketMessage",data); 
    if(data == "") return ;
    var dataView = new DataView(data)
    var n = 0;
    var type = dataView.getInt32(n,isLittleEndian);
    var length = dataView.getInt32((++n) * 4,isLittleEndian);
    var errCode = dataView.getInt32((++n) * 4,isLittleEndian);
    var frameType = dataView.getInt32((++n) * 4,isLittleEndian);
    var param2 = dataView.getInt32((++n) * 4,isLittleEndian);
    
    console.info("type=",type,"length=",length,"errCode=",errCode,"frameType=",frameType,"param2=",param2);

    //has an error
    if(type == 481){
        if(frameType == EventRecordDownloadCompleted){
            this.onRecordDownloadCompleted();
        }else{
            self.postMessage({
                type:RESPONSE_FETCH_ERROR,
                errno:frameType
            });
            //this._requestStreamFailed("the device is busy or file is not existed. errno is " + frameType);    
        }
    }else{
        if(data.byteLength >= length){
            this.parseWebSocketMessage(dataView,data,frameType,length,n);
        }
    }
}



var _readLocalFile = function(){
    if(this.localFile == null){
        console.info("_readLocalFile failed for localFile is null");
        return;
    }
    var n = 1;
    /*do{
        if(this.readOffset >= this.writeOffset){
            //console.info("this.readOffset (%d) >= this.writeOffset (%d)",this.readOffset,this.writeOffset);
            break;
        }
        this.localFile.read(this.readOffset++);
    }while(n--);*/
    do{
        this.localFile.read();
    }while(n--);
}

var _writeLocalFile = function(obj,forceWrite){
    if(this.localFile == null){
        console.info("_writeLocalFile failed for localFile is null");
        return 0;
    }
    this.batchWriteBuffer.push({
        id:     0,
        isAudio:obj.isAudio,
        pts:    obj.pts,
        index:  obj.index,
        data:   obj.data
    });

    if(this.batchWriteBuffer.length >= batchWriteLength || forceWrite){
        var copies = this.batchWriteBuffer;
        this.batchWriteBuffer = [];
        this.localFile.batchWrite(copies);
    }
    
    /*this.localFile.write({
        id:     this.writeOffset++,
        isAudio:obj.isAudio,
        pts:    obj.pts,
        index:  obj.index,
        data:   obj.data
    });*/
    //console.info("_writeLocalFile isAudio=",obj.isAudio);
}


var _onRecordDownloadCompleted = function(){
    this.websocket && this.websocket.close();
    this.websocket = null;
    if(!this.isLive){
        var forceWrite = true;
        this._writeLocalFile({
            isAudio:false,
            data:new ArrayBuffer(1),
            pts:this.duration,
            index:VideoFrameLast
         },forceWrite);
        self.postMessage({
            type:RESPONSE_FETCH_DONE,
        });
    }
}


var startWebsocket = function(){
    console.info("_startWebsocket",this.url);
    this.websocket = new WebSocket(this.url);
    this.websocket.binaryType = 'arraybuffer';
    this.websocket.onmessage = onWebSocketMessage.bind(this);
    this.websocket.onclose = onWebSocketClosed.bind(this);
    this.websocket.onerror = onWebSocketError.bind(this);
    this.websocket.onopen = onWebSocketOpened.bind(this);
}

var _initLocalFile = function(){
    if(this.isLive) return ; 
    // const fileName = Math.random().toString().substring(2)
    this.localFile = new self.DB4({callback:this});
 }

 var _closeLocalFile = function(){
    if(this.localFile !=null){
        this.localFile.release();
        this.readOffset = 0;
        this.writeOffset = 0;
        this.localFile = null;
    }
}


var _fixPTS = function(pts){ 
   if(this.beginTimeOffset == -1){
      this.beginTimeOffset = pts;
   }
   return pts - this.beginTimeOffset;
}

var fetcher = function(option){
    this.url = option.url;
    this.isLive = option.isLive;
    this.duration = option.duration;
    this.playParams = option.playParams;
    this.websocket = null;
    this.writeOffset = 0;
    this.readOffset = 0;
    this.localFile = null;
    this.beginTimeOffset = -1;
    this.isInited = false;
    this.isFirstVideoFrame = false;

    this.batchWriteBuffer = [];
    
    this.startWebsocket();
    this.initLocalFile();
};

var _FN = fetcher.prototype;

var _attributes = {
    /** start websocket */
    startWebsocket:startWebsocket,
    onWebSocketError:onWebSocketError,
    onWebSocketClosed:onWebSocketClosed,
    onWebSocketOpened:onWebSocketOpened,
    onWebSocketMessage:onWebSocketMessage,
    parseWebSocketMessage:_parseWebSocketMessage,
    /** end websocket */
    clearData:clearData,
    release:_release,
    onRecordDownloadCompleted:_onRecordDownloadCompleted,
    /**local storage */
    initLocalFile:_initLocalFile,
    _readLocalFile:_readLocalFile,
    _writeLocalFile:_writeLocalFile,
    closeLocalFile:_closeLocalFile,
    _fixPTS:_fixPTS,
    /***begin for fs.js */
    onInitSuccess:function(e){
        console.info("onFsInitSuccess",e)
     },
     onInitFailed:function(e){
         console.info("onFsInitFailed",e)
     },
     onWriteSuccess:function(e){
       //console.info("onWriteSuccess",e)
     },
     onWriteFailed:function(e){
         console.info("onFsWriteFailed",e)
     },
     onReadSuccess:function(data){
        if(data == null){ return ;}
        var postData = {
            type:REQUEST_FEED_DECODER_DATA,
            isAudio:data.isAudio,
            data:data.data,
            pts:data.pts,
            index:data.index
         };
         self.postMessage(postData,[postData.data]);
     },
     onReadFailed:function(e){
         console.info("onReadFailed",e)
     },
     onDeleteSuccess:function(e){
         console.info("onDeleteSuccess",e)
     },
     onDeleteFailed:function(e){
         console.info("onDeleteFailed",e)
     }
    
};

for(var key in _attributes){
   _FN[key] =  _attributes[key];
}

var FR = null;
self.onmessage = function (res){
    data = res.data;
    switch(data.type){
       case REQUEST_MUTEX_START:
            FR = new fetcher(data.option);
            break;
       case REQUEST_MUTEX_STOP:
            if(FR == null)return ;
            FR.release();
            FR = null;
            break;
       case REQUEST_MUTEX_DONE:
            if(FR == null)return ;
            FR.release();
            FR = null;
            break;     
            
    }
};

self.postMessage({
    type:RESPONSE_MUTEX_READY
});

/***********************END Helper **************************/